package org.xbl.xchain.sdk.amino;

import org.bitcoinj.core.Sha256Hash;
import org.bouncycastle.util.encoders.Hex;
import org.xbl.xchain.sdk.module.iccp.types.Response;
import org.xbl.xchain.sdk.types.Address;
import org.xbl.xchain.sdk.utils.AddressUtil;
import org.xbl.xchain.sdk.utils.PubkeyUtil;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.*;
import java.net.URLEncoder;
import java.util.*;

import static org.xbl.xchain.sdk.amino.TypeUtils.*;

public class Amino {
    private Map<String, byte[]> typePrefixes = new HashMap<>();
    private Map<String, Class> typeClasses = new HashMap<>();
    private Map<String, String> prefixesType = new HashMap<>();
    private AminoEncoder aminoEncoder = new AminoEncoder();
    private AminoDecoder aminoDecoder = new AminoDecoder();

    public void registerConcrete(Class clazz, String type) {
        byte[] prefixByType = getPrefixByType(type);
        typePrefixes.put(type, prefixByType);
        typeClasses.put(Hex.toHexString(prefixByType), clazz);
        prefixesType.put(Hex.toHexString(prefixByType), type);
    }

    public Class getMsgClassByType(String type) {
        byte[] prefixes = typePrefixes.get(type);
        return typeClasses.get(Hex.toHexString(prefixes));
    }

    public byte[] marshalBinaryLengthPrefixed(Object val) throws Exception {
        return aminoEncoder.marshalBinaryLengthPrefixed(val);
    }

    public byte[] marshalBinaryBare(Object val) throws Exception {
        return aminoEncoder.marshalBinaryBare(val);
    }

    public <T> T unmarshalBinaryLengthPrefixed(byte[] data, Class<T> clazz) {
        return aminoDecoder.unmarshalBinaryLengthPrefixed(data, clazz);
    }

    public <T> T unmarshalBinaryBare(byte[] data, Class<T> clazz) throws Exception {
        return aminoDecoder.unmarshalBinaryBare(new DecodeBz(data), clazz);
    }

    public class AminoDecoder {
        public <T> T unmarshalBinaryLengthPrefixed(byte[] data, Class<T> clazz) {
            DecodeBz decodeBz = new DecodeBz(data);
            VarInt.CountN countN = decodeUvarint(decodeBz);
            decodeBz.cutBytes(countN.getOffset());
            try {
                return unmarshalBinaryBare(decodeBz, clazz);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }

        public <T> T unmarshalBinaryBare(DecodeBz decodeBz, Class<T> clazz) throws Exception {
            if (isRegistered(clazz.newInstance())) {
                byte[] prefixBytes = decodeBz.popBytes(0, 4);
                decodeBz.setTempPrefix(prefixBytes);
                decodeBz.cutBytes(4);
            }
            Object o = decodeReflectBinary(decodeBz, null, clazz, 1, true, true);
            return (T) o;
        }

        public Object decodeReflectBinary(DecodeBz decodeBz, Field field, Class clazz, Integer binFieldNum, Boolean bare, Boolean forceStruct) throws Exception {
            if (isByteArray(clazz)) {
                byte[] bytes = decodeByteArray(decodeBz);
                decodeBz.cutBytes(bytes.length);
                return bytes;
            }
            if (isArrayOrList(clazz)) {
                return decodeReflectBinaryArray(decodeBz, field, binFieldNum, bare);
            }
            if (isLong(clazz)) {
                VarInt.CountN countN = decodeUvarint(decodeBz);
                decodeBz.cutBytes(countN.getOffset());
                return Long.valueOf(countN.getNum());
            }
            if (isInteger(clazz)) {
                VarInt.CountN countN = decodeUvarint(decodeBz);
                decodeBz.cutBytes(countN.getOffset());
                return Integer.valueOf(countN.getNum());
            }
            if (isBoolean(clazz)) {
                Boolean v = decodeBz.getBz()[0] == 1 ? true : false;
                decodeBz.cutBytes(1);
                return v;
            }
            if (isString(clazz)) {
                return decodeString(decodeBz);
            }
            if (isInterface(clazz)) {
                if (forceStruct) {
                    return decodeReflectBinaryStruct(decodeBz, clazz, binFieldNum, bare);
                }
                return decodeBinaryInterface(decodeBz, clazz, binFieldNum, bare);
            }
            if (isStruct(clazz)) {
                return decodeReflectBinaryStruct(decodeBz, clazz, binFieldNum, bare);
            }
            return null;
        }


        public Object decodeBinaryInterface(DecodeBz decodeBz, Class clazz, Integer binFieldNum, Boolean bare) throws Exception {
            if (!bare) {
                byte[] bytes = decodeByteArray(decodeBz);
                decodeBz.cutBytes(bytes.length);
                decodeBz = new DecodeBz(bytes);
            }
            byte[] prefixBytes = decodeBz.popBytes(0, 4);
            decodeBz.setTempPrefix(prefixBytes);
            decodeBz.cutBytes(4);
            Class aClass = typeClasses.get(Hex.toHexString(prefixBytes));
            return decodeReflectBinary(decodeBz, null, aClass, binFieldNum, true, true);
        }

        public Object decodeReflectBinaryStruct(DecodeBz decodeBz, Class clazz, Integer binFieldNum, Boolean bare) throws Exception {
            if (!bare) {
                byte[] bytes = decodeByteArray(decodeBz);
                decodeBz.cutBytes(bytes.length);
                decodeBz = new DecodeBz(bytes);
            }
            if (isBytesInterface(clazz)) {
                byte[] bytes = decodeByteArray(decodeBz);
                decodeBz.cutBytes(bytes.length);
                return new BytesInterface(bytes, prefixesType.get(Hex.toHexString(decodeBz.getTempPrefix())));
            }
            Object object = clazz.newInstance();
            Field[] fields = object.getClass().getDeclaredFields();
            for (int index = 1; index <= fields.length; index++) {
                Field field = fields[index - 1];
                if (!field.isAccessible()) {
                    field.setAccessible(true);
                }
                if (decodeBz.getBz().length == 0) {
                    Object defaultValue = getDefaultValue(field.getType());
                    field.set(object, defaultValue);
                    continue;
                }
                if (isArrayOrList(field.getType())) {
                    Object o = decodeReflectBinary(decodeBz, field, field.getType(), index, true, false);
                    field.set(object, o);
                } else {
                    FieldNumberAndTyp3 fieldNumberAndTyp3 = decodeFieldNumberAndTyp3(decodeBz);
                    if (index < fieldNumberAndTyp3.getFieldNum()) {
                        field.set(object, getDefaultValue(field.getType()));
                        continue;
                    }
                    decodeBz.cutBytes(fieldNumberAndTyp3.getOffset());

                    if (needSerialize(field)) {
                        SerializeType serializeType = getSerializeType(field);
                        Object o = decodeReflectBinary(decodeBz, field, serializeType.getDeSerializeclazz(), index, false, false);
                        String v = fieldDeSerialize(decodeBz, field, o);
                        field.set(object, v);
                        continue;
                    }
                    Object o = decodeReflectBinary(decodeBz, field, field.getType(), index, false, false);
                    field.set(object, o);
                }
            }
            return object;
        }

        public Object decodeReflectBinaryArray(DecodeBz decodeBz, Field field, Integer binFieldNum, Boolean bare) throws Exception {
            if (!bare) {
                byte[] bytes = decodeByteArray(decodeBz);
                decodeBz.cutBytes(bytes.length);
                decodeBz = new DecodeBz(bytes);
            }
            Class elementType = getArrayElementType(field);
            List list = new ArrayList<>();
            while (true) {
                FieldNumberAndTyp3 fieldNumberAndTyp3 = decodeFieldNumberAndTyp3(decodeBz);
                if (!fieldNumberAndTyp3.getSuccess() || binFieldNum != fieldNumberAndTyp3.getFieldNum()) {
                    break;
                }
                decodeBz.cutBytes(fieldNumberAndTyp3.getOffset());
                if (decodeBz.getBz().length > 0 && decodeBz.getBz()[0] == 0x00) {
                    decodeBz.cutBytes(1);
                    list.add(getDefaultValue(elementType));
                    continue;
                }
                Object o = decodeReflectBinary(decodeBz, field, elementType, 1, false, false);
                list.add(o);
            }
            if (isArray(field.getType())) {
                return list.toArray((Object[]) Array.newInstance(elementType, list.size()));
            }
            return list;
        }

        public byte[] decodeByteArray(DecodeBz bz) {
            byte[] val = new byte[0];
            try {
                VarInt.CountN countN = decodeUvarint(bz);
                bz.cutBytes(countN.getOffset());
                val = bz.popBytes(0, countN.getNum());
            } catch (Exception e) {
                return new byte[]{};
            }
            return val;
        }

        public String decodeString(DecodeBz bz) {
            byte[] val = decodeByteArray(bz);
            bz.cutBytes(val.length);
            return new String(val);
        }

        public VarInt.CountN decodeUvarint(DecodeBz bz) {
            return VarInt.readUVarInt(bz.getBz());
        }

        private String fieldDeSerialize(DecodeBz decodeBz, Field field, Object object) throws Exception {
            AminoFieldSerialize aminoFieldSerialize = field.getDeclaredAnnotation(AminoFieldSerialize.class);
            String format = aminoFieldSerialize.format();
            switch (format) {
                case "address":
                    return AddressUtil.accAddressToBech32("xchain", (byte[]) object);
                case "pubkey":
                    return PubkeyUtil.Bech32ifyPubKey(Address.GetBech32ConsensusPubPrefix(), (BytesInterface) object);
                case "time":
                    return ((GoTime) object).toTimeStr();
            }
            return null;
        }

        public FieldNumberAndTyp3 decodeFieldNumberAndTyp3(DecodeBz decodeBz) {
            VarInt.CountN countN = null;
            try {
                countN = decodeUvarint(decodeBz);
            } catch (Exception e) {
                return FieldNumberAndTyp3.faild();
            }
            Integer value = countN.getNum();
            Integer typ3 = value & 0x07;

            Integer num = value >> 3;

            if (num > (1 << 29 - 1)) {
                return FieldNumberAndTyp3.faild();
            }
            return FieldNumberAndTyp3.success(num, typ3, countN.getOffset());
        }

        private Object getDefaultValue(Class clazz) {
            if (isByteArray(clazz)) {
                return new byte[]{};
            }
            if (isArray(clazz)) {
                return Array.newInstance(clazz.getComponentType(), 0);
            }
            if (isList(clazz)) {
                return null;
            }
            if (isString(clazz)) {
                return "";
            }
            if (Response.class.isAssignableFrom(clazz)) {
                return new Response();
            }
            if (clazz.isPrimitive() && (isInteger(clazz) || isLong(clazz))) {
                return 0;
            }
            if (clazz.isPrimitive() && isBoolean(clazz)) {
                return false;
            }
            return null;
        }

        private Class getArrayElementType(Field field) {
            Type genericType = field.getGenericType();
            if (genericType instanceof ParameterizedType) {
                ParameterizedType pt = (ParameterizedType) genericType;
                Class<?> actualTypeArgument = (Class<?>) pt.getActualTypeArguments()[0];
                return actualTypeArgument;
            } else {
                return field.getType().getComponentType();
            }
        }
    }

    public class AminoEncoder {
        public byte[] marshalBinaryLengthPrefixed(Object val) throws Exception {
            if (val == null) {
                throw new Exception("unsupported type");
            }
            byte[] dataBytes = marshalBinaryBare(val);
            byte[] lengthPrefixed = encodeUvarint(dataBytes.length);
            byte[] contact = contact(lengthPrefixed, dataBytes);
            return contact;
        }

        public byte[] marshalBinaryBare(Object val) throws Exception {
            byte[] dataBytes = encodeReflectBinary(val, 1, true, true);

            if (isRegistered(val)) {
                byte[] prefixBytes = typePrefixes.get(getClassType(val));
                dataBytes = contact(prefixBytes, dataBytes);
            }
            return dataBytes;
        }

        public byte[] encodeReflectBinary(Object val, Integer binFieldNum, Boolean bare, Boolean forceStruct) throws Exception {
            Class<?> valClass = val.getClass();
            if (isByteArray(valClass)) {
                return encodeByteArray((byte[]) val);
            }
            if (isArrayOrList(valClass)) {
                return encodeBinaryArray(val, binFieldNum, bare);
            }
            if (isNumber(valClass)) {
                return encodeNumber(val);
            }
            if (isBoolean(valClass)) {
                return encodeBoolean((Boolean) val);
            }
            if (isString(valClass)) {
                return encodeString((String) val);
            }
            if (isInterface(valClass)) {
                if (forceStruct) {
                    return encodeBinaryStruct(val, bare);
                }
                return encodeBinaryInterface(val, binFieldNum, bare);
            }
            if (isStruct(valClass)) {
                return encodeBinaryStruct(val, bare);
            }
            return null;
        }

        public byte[] encodeBinaryInterface(Object val, Integer binFieldNum, Boolean bare) throws Exception {
            ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
            if (!isRegistered(val)) {
                throw new Exception("unregistry interface");
            }
            byte[] prefix = typePrefixes.get(getClassType(val));
            byteBuffer.write(prefix);
            byte[] dataBytes = encodeReflectBinary(val, binFieldNum, true, true);
            byteBuffer.write(dataBytes);

            byte[] array = byteBuffer.toByteArray();

            if (bare) {
                return array;
            } else {
                return encodeByteArray(array);
            }
        }

        public byte[] encodeBinaryStruct(Object val, Boolean bare) throws Exception {
            if (isBytesInterface(val.getClass())) {
                return encodeReflectBinary(getBytesInterfaceValue(val), 1, false, false);
            }
            ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
            Field[] fields = val.getClass().getDeclaredFields();
            for (int index = 1; index <= fields.length; index++) {
                Field field = fields[index - 1];
                if (!field.isAccessible()) {
                    field.setAccessible(true);
                }
                Object o = field.get(val);
                if (isDefaultValue(o)) {
                    continue;
                }
                if (needSerialize(field)) {
                    o = fieldSerialize(field, o);
                }
                if (isArrayOrList(o.getClass())) {
                    byteBuffer.write(encodeBinaryArray(o, index, true));
                } else {
                    byte[] fieldNumberAndTyp3 = encodeFieldNumberAndTyp3(index, typeToTyp3(o.getClass()));
                    byteBuffer.write(fieldNumberAndTyp3);
                    byte[] bytes = encodeReflectBinary(o, index, false, false);
                    byteBuffer.write(bytes);
                }
            }
            byte[] array = byteBuffer.toByteArray();

            if (bare) {
                return array;
            } else {
                return encodeByteArray(array);
            }
        }

        public byte[] encodeBinaryArray(Object val, Integer binFieldNum, Boolean bare) throws Exception {
            ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();

            Object[] encodeArray = null;
            if (isArray(val.getClass())) {
                encodeArray = (Object[]) val;
            } else {
                encodeArray = ((List) val).toArray();
            }
            for (Object item : encodeArray) {
                byteBuffer.write(encodeFieldNumberAndTyp3(binFieldNum, 2));
                if (isDefaultValue(item)) {
                    byteBuffer.write(Byte.valueOf("00", 16));
                } else {
                    byteBuffer.write(encodeReflectBinary(item, 1, false, false));
                }
            }
            byte[] byteArray = byteBuffer.toByteArray();
            if (bare) {
                return byteArray;
            } else {
                return encodeByteArray(byteArray);
            }
        }

        public byte[] encodeNumber(Object val) {
            if (val instanceof Integer) {
                return encodeUvarint((Integer) val);
            }
            return encodeUvarint((Long) val);
        }

        public byte[] encodeBoolean(Boolean val) {
            return encodeNumber(val ? 1L : 0);
        }

        public byte[] encodeByteArray(byte[] bs) {
            return contact(encodeUvarint(bs.length), bs);
        }

        public byte[] encodeString(String val) {
            return encodeByteArray(val.getBytes());
        }

        public byte[] encodeUvarint(int u) {
            return VarInt.putUvarint(u);
        }

        public byte[] encodeUvarint(long u) {
            return VarInt.putUvarint(u);
        }

        private Object fieldSerialize(Field field, Object val) throws Exception {
            AminoFieldSerialize aminoFieldSerialize = field.getDeclaredAnnotation(AminoFieldSerialize.class);
            String format = aminoFieldSerialize.format();
            switch (format) {
                case "address":
                    return AddressUtil.accAddressFromBech32((String) val);
                case "pubkey":
                    return PubkeyUtil.decodeBech32ifyPubKey((String) val);
                case "time":
                    return new GoTime((String) val);
            }
            return val;
        }

        public byte[] encodeFieldNumberAndTyp3(int index, int typ3) throws IOException {
            int v = index << 3 | typ3;
            return encodeUvarint(v);
        }

        private Boolean isDefaultValue(Object o) {
            if (o == null) {
                return true;
            }
            if (isByteArray(o.getClass())) {
                return ((byte[]) o).length == 0;
            }
            if (isArray(o.getClass())) {
                return ((Object[]) o).length == 0;
            }
            if (isList(o.getClass())) {
                return ((List) o).size() == 0;
            }
            if (isString(o.getClass())) {
                return ((String) o).length() == 0;
            }
            return false;
        }

        private byte[] getBytesInterfaceValue(Object o) throws Exception {
            Field field = o.getClass().getDeclaredField("value");
            if (!field.isAccessible()) {
                field.setAccessible(true);
            }
            return (byte[]) field.get(o);
        }

        private byte typeToTyp3(Class clazz) {
            if (isNumber(clazz)) {
                return 0;
            }
            return 2;
        }
    }

    public boolean isRegistered(Object o) {
        return typePrefixes.containsKey(getClassType(o)) || typeClasses.containsValue(o.getClass());
    }

    private Boolean needSerialize(Field field) {
        return field.getDeclaredAnnotation(AminoFieldSerialize.class) != null;
    }

    private SerializeType getSerializeType(Field field) {
        AminoFieldSerialize aminoFieldSerialize = field.getDeclaredAnnotation(AminoFieldSerialize.class);
        String format = aminoFieldSerialize.format();
        return SerializeType.valueOfByType(format);
    }

    private String getClassType(Object o) {
        String type = null;
        try {
            Method method = o.getClass().getDeclaredMethod("type");
            if (!method.isAccessible()) {
                method.setAccessible(true);
            }
            type = (String) method.invoke(o);
        } catch (Exception e) {
            return null;
        }

        return type;
    }

    private byte[] getPrefixByType(String type) {
        byte[] hash = Sha256Hash.hash(type.getBytes());
        int i = 0;
        while (hash[i] == 0x00) {
            i++;
        }
        i += 3;
        while (hash[i] == 0x00) {
            i++;
        }
        byte[] prefix = new byte[4];
        System.arraycopy(hash, i, prefix, 0, 4);
        return prefix;
    }

    private byte[] contact(byte[] bs1, byte[] bs2) {
        byte[] data3 = new byte[bs1.length + bs2.length];
        System.arraycopy(bs1, 0, data3, 0, bs1.length);
        System.arraycopy(bs2, 0, data3, bs1.length, bs2.length);
        return data3;
    }
}
